/**
 * A generic tool for picking objects with the mouse.
 **/

#ifndef __tools_object_selectionH__
#define __tools_object_selectionH__

#include "physics.h"
#include "graphics_functions.h"
#include "input.h"
#include "render.h"

namespace WtfEngine {
	/**
	 * Thrown when object is clicked on.
	 **/
	class ObjectMouseDownEvent: public KeyedObjectEvent<ObjectMouseDownEvent> {
		tPosition mvRelPos;

	public:
		ObjectMouseDownEvent(const IGameObject::tRef& rObj, const tPosition& rRelPos)
		: KeyedObjectEvent<ObjectMouseDownEvent>(rObj), mvRelPos(rRelPos) {
		};

		inline const tPosition& getRelativePosition() const {
			return mvRelPos;
		};
	};

	/**
	 * Thrown when mouse is lifted off the object.
	 **/
	class ObjectMouseUpEvent: public KeyedObjectEvent<ObjectMouseUpEvent> {
	public:
		ObjectMouseUpEvent(const IGameObject::tRef& rObj)
		: KeyedObjectEvent<ObjectMouseUpEvent>(rObj) {
		};
	};

	/**
	 * Thrown when object is selected.
	 **/
	class ObjectSelectionEvent: public KeyedObjectEvent<ObjectSelectionEvent> {
	public:
		ObjectSelectionEvent(const IGameObject::tRef& rObj)
		: KeyedObjectEvent<ObjectSelectionEvent>(rObj) {
		};
	};

	/**
	 * Thrown when object is deselected.
	 **/
	class ObjectDeselectionEvent: public KeyedObjectEvent<ObjectDeselectionEvent> {
	public:
		ObjectDeselectionEvent(const IGameObject::tRef& rObj)
		: KeyedObjectEvent<ObjectDeselectionEvent>(rObj) {
		};
	};


#	define SELECTIONBOX_DZ				0.5
#	define SELECTIONBOX_HEIGHT			2.0
#	define SELECTIONBOX_HEIGHT_SLOP	0.2

	template <class ObjectT>
	class ObjectSelection: public IRenderable, public IDynamic, public GcObject {
	public:
		typedef ObjectSelection<ObjectT>         tThis;
		typedef std::set<typename ObjectT::tRef> tObjectList;

	protected:
		bool						mbIsSelecting;
		tPosition					mvSelectionStart;
		tPosition					mvSelectionEnd;
		typename ObjectT::tRef mrObjectHover;
		tObjectList            mObjectList;
	
		IFunctor<>::tRef mrOnChange;

		/// Draws unit selection boxes
		virtual void Render() {
			btVector3	aabbMin, aabbMax;
			WorldObject * pWorldUnit;

			WtfGraphics::EnableBlending();

			// Render selection box
			if(mbIsSelecting && (mvSelectionEnd != mvSelectionStart)) {
				WtfGraphics::EnableWireframe(0.1);
				WtfGraphics::SaveView();
				
				glColor4f(0.2, 0.2, 0.75, 0.5);
				WtfGraphics::Translate(mvSelectionStart);
				WtfGraphics::DrawBox(tVector(), mvSelectionEnd - mvSelectionStart);

				WtfGraphics::RestoreView();
				WtfGraphics::DisableWireframe();
			};

			WtfGraphics::DisableBlending();
		};

		virtual void Update() {
			tVector		vPos;
			iVector2D	vMousePos(InputHandler::GetInstance().GetMousePosition());

			if(mbIsSelecting && (mvSelectionStart != mvSelectionEnd)) {
				
				//
				// Dragging a selection box around units. Use a convex sweep test
				// to determine units we have selected. We have a start and end position
				// for the box in world coordinates.
				//

				struct SelectionCallback: public btCollisionWorld::ConvexResultCallback {
					tThis * pUnitSelection;

					SelectionCallback(tThis * p): btCollisionWorld::ConvexResultCallback(),
						pUnitSelection(p) {
					};

					btScalar	addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) {
						// If the collision object is an RTS unit and it is group selectable, then add it
						// to the unit selection.
						typename ObjectT::tRef rUnit = dynamic_cast<ObjectT *>(convexResult.m_hitCollisionObject);
						if(!rUnit.isNull()) {
							pUnitSelection->Select(rUnit);
						};

						return convexResult.m_hitFraction;
					};
				};

				btTransform start, end;
				tVector	extents(mvSelectionEnd[0] - mvSelectionStart[0], mvSelectionEnd[1] - mvSelectionStart[1], 0);
				extents = extents / 2;

				start.setIdentity();
				start.setOrigin(FROMVECT(mvSelectionStart + extents));
				end.setIdentity();
				end.setOrigin(FROMVECT(mvSelectionStart) + btVector3(extents[0], extents[1], mvSelectionEnd[2] - mvSelectionStart[2]));

				btBoxShape	box(btVector3(abs(extents[0]), abs(extents[1]), abs(extents[2])));
				
				// Perform the sweep test to find units.
				mObjectList.clear();
				SelectionCallback callback(this);
				PhysicsEngine::GetInstance().getWorld()->convexSweepTest(&box, start, end, callback);
			} else {

				//
				// Check for units under the mouse. If there is one, set the unit hover.
				//

				tRay ray = Renderer::GetInstance().GetCamera()->PixelToRay(Renderer::GetInstance().GetScreen(), vMousePos);

				WorldObject::tRef rObj = PhysicsEngine::GetInstance().RayCast(ray, vPos);
				if(!rObj.isNull()) {
					mrObjectHover = dynamic_cast<ObjectT*>(&*rObj);
				};
			};
		};

		virtual void OnMouseClick() {
			tRay				ray;
			WorldObject::tRef	rObj;
			tVector				vPos;
			MouseUpEvent *	pEv = Kernel::GetInstance().GetCurrentEvent<MouseUpEvent>();

			if(!pEv) return;

			switch(pEv->getButton()) {
				case MOUSE_LEFT:
					
					//
					// Left mouse button selects units
					//

					if(mbIsSelecting && (mvSelectionStart != mvSelectionEnd)) {
						// For drag selection - units are already selected
						mbIsSelecting = false;
						break;
					};

					ray = Renderer::GetInstance().GetCamera()->PixelToRay(Renderer::GetInstance().GetScreen(),
						iVector2D(pEv->x(), pEv->y()));
					

					rObj = PhysicsEngine::GetInstance().RayCast(ray, vPos);
					if(!rObj.isNull()) {
						ObjectT * pUnit = dynamic_cast<ObjectT*>(&*rObj);
						if(pUnit) {
							// Select the unit
							mbIsSelecting = false;
							this->Clear();
							this->Select(pUnit);
							break;
						};
					};

					// Left click on nothing - clear selection
					mbIsSelecting = false;
					this->Clear();
					break;
			};
		};

		virtual void OnMouseDown() {
			MouseDownEvent *	pEv = Kernel::GetInstance().GetCurrentEvent<MouseDownEvent>();
			if(pEv && pEv->getButton() == MOUSE_LEFT) {
				//
				// Set start world position for selection box
				//

				tRay ray = Renderer::GetInstance().GetCamera()->PixelToRay(Renderer::GetInstance().GetScreen(), InputHandler::GetInstance().GetMousePosition());
				WorldObject::tRef rObj = PhysicsEngine::GetInstance().RayCast(ray, mvSelectionStart);
				mvSelectionEnd = mvSelectionStart;

				mbIsSelecting = (rObj.isNull() ? false :
					(Kernel::GetInstance().ProcessEvent(
						new ObjectMouseDownEvent(rObj, mvSelectionStart - rObj->getPosition())) != Event::STOPPED));
			};
		};

		virtual void OnMouseMove() {
			if(mbIsSelecting) {

				//
				// Set end world position for selection box
				//

				tRay ray = Renderer::GetInstance().GetCamera()->PixelToRay(Renderer::GetInstance().GetScreen(), InputHandler::GetInstance().GetMousePosition());
				PhysicsEngine::GetInstance().RayCast(ray, mvSelectionEnd);

				if(mvSelectionStart[1] <= (mvSelectionEnd[1] + SELECTIONBOX_HEIGHT_SLOP))
					mvSelectionEnd += tVector(0, SELECTIONBOX_HEIGHT, 0);
			};
		};

		virtual void OnSelectionChange() {
			if(!mrOnChange.isNull()) {
				(*mrOnChange)();
			};
		};

	public:
		ObjectSelection(): GcObject(), mbIsSelecting(false) {
		};

		inline void Select(const typename ObjectT::tRef& rUnit) {
			if(std::find(mObjectList.begin(), mObjectList.end(), rUnit) == mObjectList.end()) {
				mObjectList.insert(rUnit);
				Kernel::GetInstance().ProcessEvent(new ObjectSelectionEvent(rUnit));
				OnSelectionChange();
			};
		};

		inline void Clear() {
			for(tObjectList::iterator i = mObjectList.begin(); i != mObjectList.end(); i++) {
				Kernel::GetInstance().ProcessEvent(new ObjectDeselectionEvent(*i));
			};
			mObjectList.clear();
			OnSelectionChange();
		};

		void Init() {
			GameState::tRef rGameState = Kernel::GetInstance().GetGameState();
			
			IDynamic::Init();
			Renderer::GetInstance().PushObject(this);

			rGameState->BindEvent(new MouseUpEvent(),
					new ObjFunctor<tThis>(this, &tThis::OnMouseClick));
			rGameState->BindEvent(new MouseDownEvent(),
					new ObjFunctor<tThis>(this, &tThis::OnMouseDown));
			rGameState->BindEvent(new MouseMoveEvent(),
					new ObjFunctor<tThis>(this, &tThis::OnMouseMove));
		};

		void Destroy() {
			GameState::tRef rGameState = Kernel::GetInstance().GetGameState();

			rGameState->RemoveHandler(new ObjFunctor<tThis>(this, &tThis::OnMouseClick));
			rGameState->RemoveHandler(new ObjFunctor<tThis>(this, &tThis::OnMouseDown));
			rGameState->RemoveHandler(new ObjFunctor<tThis>(this, &tThis::OnMouseMove));

			Renderer::GetInstance().RemoveObject(this);
			IDynamic::Destroy();
		};

		const tObjectList& getSelection() const {
			return mObjectList;
		};

		const IFunctor<>::tRef& getOnChange() const {
			return mrOnChange;
		};
		void setOnChange(const IFunctor<>::tRef& rF) {
			mrOnChange = rF;
		};

		GC_AUTOSIZE(ObjectSelection);
	};
};

#endif
